package apis;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.util.StringUtils;

import play.Logger;
import play.libs.F.Promise;
import play.libs.WS;
import play.libs.WS.Response;
import play.libs.WS.WSRequestHolder;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class IDOLApi {

	// from the environment
//	public static final String IDOL_API_URL = System.getenv("IDOL_API_URL");
//	public static final String IDOL_API_VERSION = System.getenv("IDOL_API_VERSION");
//	public static final String IDOL_API_APIKEY = System.getenv("IDOL_API_APIKEY");
	;
	public static final String IDOL_API_URL = "https://api.idolondemand.com/1/api";
	public static final String IDOL_API_VERSION = "v1";
	public static final String IDOL_API_APIKEY = "bde36db0-cbed-44e0-8d71-9bfc4902b038";
	
    public static final String IDOL_APIKEY = "apikey";
    public static final String SYNC = "/sync";
    public static final String ASYNC = "/async";
    
    // Indexing APIs
    public static final String LIST_INDEXES_PATH = "/listindexes";
    public static final String CREATE_TEXT_INDEX_PATH = "/createtextindex";
    public static final String DELETE_TEXT_INDEX_PATH = "/deletetextindex";
    public static final String DELETE_FROM_TEXT_INDEX_PATH = "/deletefromtextindex";
    public static final String ADD_TO_TEXT_INDEX_PATH = "/addtotextindex";
    public static final String INDEX_STATUS_PATH = "/indexstatus";
    // Text Analysis APIs
    public static final String LANGUAGE_IDENTIFICATION_PATH = "/identifylanguage";
    public static final String SENTIMENT_ANALYSIS_PATH = "/analyzesentiment";
    public static final String ENTITY_EXTRACTION = "/extractentities";
    // Search APIs
    public static final String FIND_SIMILAR_PATH = "/findsimilar";
    public static final String FIND_RELATED_CONCEPTS_PATH = "/findrelatedconcepts";
    public static final String QUERY_TEXT_INDEX_PATH = "/querytextindex";
    // Format Conversion APIs
    public static final String OCR_DOCUMENT = "ocrdocument";
    
    // language enum map used to convert Language to LanguageCode
    private static Map<String, IDOL.LanguageCode> languageToCodeMap = null;
    

    /**
     * Build field_text parameter for IDOL search
     *
     * @param op IDOL.FieldTextOperator
     * @param field the field name in the IDOL document
     * @param value the value that operator applies to
     @ return field_text parameter that can be passed to IDOL search request field_text parameter
     */
    public static String getFieldText(IDOL.FieldTextOperator op, String field, String value) {
        if (!StringUtils.hasText(value)) {
            return op.name()+"{"+value+"}:"+field;
        } else {
            return op.name()+"{}:"+field;
        }
    }

    /**
     * Build date range field_text parameter for IDOL search
     *
     * @param field the field name in the IDOL document
     * @param startDate the start date
     * @param endDate the end date
     @ return date range field_text parameter that can be passed to IDOL search request field_text parameter
     */
    public static String getFieldTextDateRange(String field, Date startDate, Date endDate) {
//        return IDOL.FieldTextOperator.RANGE.name()+"{"+startDate.format()+","+endDate.format()+"}:"+field;
    	return null;
    }
    
    /**
     * Find similar documents in the index database
     *
     * @param index the index database name to search
     * @param text the search text
     * @param print control which fields to return in the response
     * @param fieldText FieldText operators that define fields with condition the resulting document must contain
     */
    public static List<IDOL.DocumentOutput> findSimilar(String index, String text, IDOL.Print print, String fieldText) {
        String path = SYNC+FIND_SIMILAR_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (index == null || text == null) {
            throw new IDOLException("similarFind: index and text are required");
        }
        params.put("text", text);
        params.put("database_match", index);
        if (print != null) {
            params.put("print", print.name());
        }
        if (!StringUtils.hasText(fieldText)) {
            params.put("field_text", fieldText);
        }
        IDOL.DocumentOutputs searchResp = (IDOL.DocumentOutputs) invokeIDOLApi(path, "GET", params, IDOL.DocumentOutputs.class);
        return searchResp.documents;
    }
        
    /**
     * Find related concepts in index database
     *
     * @param index the index database name to search
     * @param text the search text
     * @param print control which fields to return in the response
     * @param fieldText FieldText operators that define fields with condition the resulting document must contain
     */
    public static List<IDOL.Entities> findRelatedConcepts(String index, String text, String fieldText) {
        String path = SYNC+FIND_RELATED_CONCEPTS_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (index == null || text == null) {
            throw new IDOLException("findRelatedConcepts: index and text are required");
        }
        params.put("text", text);
        params.put("database_match", index);
        if (!StringUtils.hasText(fieldText)) {
            params.put("field_text", fieldText);
        }
        IDOL.FindRelatedConceptsResponse searchResp = (IDOL.FindRelatedConceptsResponse) invokeIDOLApi(path, "GET", params, IDOL.FindRelatedConceptsResponse.class);
        return searchResp.entities;
    }

    /**
     * Search text index database
     *
     * @param index the index database name to search
     * @param text the search text
     * @param print control which fields to return in the response
     * @param fieldText FieldText operators that define fields with condition the resulting document must contain
     */
    public static List<IDOL.DocumentOutput> searchTextIndex(String index, String text, IDOL.Print print, String fieldText) {
        String path = SYNC+QUERY_TEXT_INDEX_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (index == null || text == null) {
            throw new IDOLException("searchTextIndex: index and text are required");
        }
        params.put("text", text);
        params.put("database_match", index);
        if (print != null) {
            params.put("print", print.name());
        }
        if (!StringUtils.hasText(fieldText)) {
            params.put("field_text", fieldText);
        }
        IDOL.DocumentOutputs searchResp = (IDOL.DocumentOutputs) invokeIDOLApi(path, "GET", params, IDOL.DocumentOutputs.class);
        return searchResp.documents;
    }
        
    /** 
     * List indexes
     *
     * @return IDOL.ListIndexesResponse
     * @throws IDOLException
     */
    public static IDOL.ListIndexesResponse listIndexes() {
        String path = SYNC+LIST_INDEXES_PATH;
        IDOL.ListIndexesResponse listIndexResp = (IDOL.ListIndexesResponse) invokeIDOLApi(path, "GET", null, IDOL.ListIndexesResponse.class);
        return listIndexResp;
    }
    
    /**
     * Create text index
     *
     * @param index the index name to create
     * @param flavor the flavor of index
     * @param description the description of index
     * @return IDOL.CreateTextIndexResponse
     * @throws IDOLException
     */
    public static IDOL.CreateTextIndexResponse createTextIndex(String index, IDOL.IndexFlavor flavor, String description) {
        String path = SYNC+CREATE_TEXT_INDEX_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (index == null || flavor == null) {
            throw new IDOLException("createTextIndex: index and flavor are required");
        }
        params.put("index", index);
        params.put("flavor", flavor.name());
        if (description != null) {
            params.put("description", description);
        }
        IDOL.CreateTextIndexResponse createIndexResp = (IDOL.CreateTextIndexResponse) invokeIDOLApi(path, "GET", params, IDOL.CreateTextIndexResponse.class);
        return createIndexResp;
    }
    
    /**
     * Language identification
     *
     * @param source IDOL input source type
     * @param input input text to identify language
     * @return IDOL.LanguageIdentificationResponse
     * @throws IDOLException
     */
    public static IDOL.LanguageIdentificationResponse languageIdentification(IDOL.InputSource source, String input) {
        String path = SYNC+LANGUAGE_IDENTIFICATION_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (source == null || input == null) {
            throw new IDOLException("languageIdentification: both source and input are required");
        }
        params.put(source.name(), input);   // use text for text input

        IDOL.LanguageIdentificationResponse identficationResp = (IDOL.LanguageIdentificationResponse) invokeIDOLApi(path, "GET", params, IDOL.LanguageIdentificationResponse.class);
        return identficationResp;
    }
    
    /**
     * Sentiment analysis
     *
     * @param source IDOL input source type
     * @param input input text to analysis
     * @param language the three-char language code, this is three-char language code, it"s different IDOL.Language.
     *          It would be better if they are same.
     * @return IDOL.SentimentAnalysisResponse
     * @throws IDOLException
     */
    public static IDOL.SentimentAnalysisResponse sentimentAnalysis(IDOL.InputSource source, String input, IDOL.LanguageCode language) {
        String path = SYNC+SENTIMENT_ANALYSIS_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (source == null || input == null) {
            throw new IDOLException("sentimentAnalysis: both source and input are required");
        }
        params.put(source.name(), input);
        if (language != null) {
            params.put("language", language.name());
        }

        IDOL.SentimentAnalysisResponse sentimentResp = (IDOL.SentimentAnalysisResponse) invokeIDOLApi(path, "GET", params, IDOL.SentimentAnalysisResponse.class);
        return sentimentResp;
    }
    
    /**
     * Add to text index
     *
     * @param index the index name to add text
     * @param source input source type
     * @param  input input text to add to the index
     * @param  duplicate duplicate mode option
     * @return IDOL.AddToTextIndexResponse
     * @throws IDOLException
     */
    public static IDOL.AddToTextIndexResponse addToTextIndex(String index, IDOL.InputSource source, IDOL.DocumentInput documentInput, IDOL.DuplicateMode duplicate) {
        String path = SYNC+ADD_TO_TEXT_INDEX_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (index == null || source == null || documentInput == null) {
            throw new IDOLException("addToTextIndex: index, source and documentInput are required");
        }
        params.put("index", index);
        // JSON.serialize() produces "null" string for null value, so use JSONGenerator to customize serialization
        String jsonStr = null;
        try {
        	jsonStr = serializeDocumentInput(documentInput);
        } catch (IOException e) {
        	
        }
        Logger.info("input doc json: "+jsonStr);
        params.put(source.name(), jsonStr); // it should be json for text input
        if (duplicate != null) {
            params.put("duplicate_mode", duplicate.name());
        }

        IDOL.AddToTextIndexResponse addToIndexResp = (IDOL.AddToTextIndexResponse) invokeIDOLApi(path, "GET", params, IDOL.AddToTextIndexResponse.class);
        return addToIndexResp;
    }
    
    /**
     * Delete text index
     *
     * @param index index name to delete
     * @param confirm the confirm hash returned in the first delete index request
     * @return IDOL.DeleteTextIndexResponse
     * @throws IDOLException
     */
    public static IDOL.DeleteTextIndexResponse deleteTextIndex(String index, String confirm) {
        String path = SYNC+DELETE_TEXT_INDEX_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (index == null) {
            throw new IDOLException("deleteTextIndex: index is required");
        }
        params.put("index", index);
        if (confirm != null) {
            params.put("confirm", confirm);
        }

        IDOL.DeleteTextIndexResponse deleteIndexResp = (IDOL.DeleteTextIndexResponse) invokeIDOLApi(path, "GET", params, IDOL.DeleteTextIndexResponse.class);
        return deleteIndexResp;
    }
    
    /**
     * Index status
     *
     * @param index the index name to get status from
     * @return IDOL.IndexStatusResponse
     * @throws IDOLException
     */
    public static IDOL.IndexStatusResponse indexStatus(String index) {
        String path = SYNC+INDEX_STATUS_PATH;
        Map<String, String> params = new HashMap<String, String>();
        if (index == null) {
            throw new IDOLException("indexStatus: index is required");
        }
        params.put("index", index);

        IDOL.IndexStatusResponse indexStatusResp = invokeIDOLApi(path, "GET", params, IDOL.IndexStatusResponse.class);
        return indexStatusResp;
    }
    
    /**
     * The language enum in the SentimentAnalysis request is different than the one returned in the LanguageIdentification response.
     * The method converts the language enum in LanguageIdentification response to three-char language code, so that can be used in 
     *    SentimentAnalysis request. The two APIs should use the same language code!
     *
     * @param language the language enum returned from Language Identification
     * return the corresponding three-char language code
     */
    public static IDOL.LanguageCode getLanguageCode(String language) {
        if (languageToCodeMap == null) {    // create conversion map if it hasn"t been created yest
            languageToCodeMap = new HashMap<String, IDOL.LanguageCode>();
            // For now, just handle 6 languages{eng, fre, spa, ger, ita, chi} in the API doc
            languageToCodeMap.put(IDOL.Language.english.name(), IDOL.LanguageCode.eng);
            languageToCodeMap.put(IDOL.Language.french.name(), IDOL.LanguageCode.fre);
            languageToCodeMap.put(IDOL.Language.spanish.name(), IDOL.LanguageCode.spa);
            languageToCodeMap.put(IDOL.Language.german.name(), IDOL.LanguageCode.ger);
            languageToCodeMap.put(IDOL.Language.italian.name(), IDOL.LanguageCode.ita);
            languageToCodeMap.put(IDOL.Language.chinese.name(), IDOL.LanguageCode.chi);
        }
        
        IDOL.LanguageCode code = languageToCodeMap.get(language);
        return code;
    }
    
    /**
     * Send IDOL request to IDOL API server
     *
     * @return the deserialized IDOL response or Object if deserialization is not possible.
     * @throws IDOLException
     */
    private static <T> T invokeIDOLApi(String path, String method, Map<String, String> params, Class<T> responseType) {
        // add api version, the path is sync/async+action
        path = path + "/" + IDOL_API_VERSION;
        if (params == null) {
            params = new HashMap<String, String>();
        }
        // add apikey
        WSRequestHolder reqHolder = WS.url(IDOL_API_URL+path)
    			.setContentType("application/json")
    			.setQueryParameter(IDOL_APIKEY, IDOL_API_APIKEY);
        
        if (params != null && !params.isEmpty()) {
        	for (String key : params.keySet()) {
        		reqHolder = reqHolder.setQueryParameter(key, params.get(key));
        	}
        }
        
    	Promise<Response> promise = reqHolder.get();
    	Response resp = promise.get(20000);		// 20 sec timeout!
    	Logger.info(resp.getBody());
    	
    	ObjectMapper mapper = new ObjectMapper();
    	try {
    		T respObject = mapper.readValue(resp.getBody(), responseType);
    		return respObject;
    	} catch (Exception e) {
    		// handle exception
    		Logger.error("IDOL API Exception: "+e);
    		Logger.info(resp.getBody());
    		throw new IDOLException("IDOL request failed with "+e.getMessage());
    	}

        
/*        
        // send request to IDOL API server
        HttpResponse response = client.sendRequest(path, method, params);
        
        // 300 is arbitrary number to prevent cache growing
        if (!Test.isRunningTest() && apiConfig.API_Logging__c && apiLogCache.size() < 300) {
            String name = method+" "+path;
            String reqLog = JSON.serializePretty(params);
            String respLog = response.getStatusCode()+": "+response.getBody();
            IDOL_API_Log__c log = new IDOL_API_Log__c(Name=name, Request__c=reqLog, Response__c=respLog);
            apiLogCache.add(log);
        }

        // handle error and rate limit
        if (response.getStatusCode() == 200) {  // no error
            if (returnType != null) {
                Object returnObject = JSON.deserialize(response.getBody(), returnType);
                return returnObject;
            }
            // if response can"t be deserialized, just return body string
            return response.getBody();
        } else {
            System.debug("IDOL API Error: "+response.getBody());
            // Unfortunately IDOL error responses have many different variations, can"t use deerailize method. Use JSON.deserializeUntyped() instead.
            // It would be better API returns an uniform error response with well-defined schema.
            // IDOL.ErrorResponse errorResponse = (IDOL.ErrorResponse)JSON.deserialize(response.getBody(), IDOL.ErrorResponse.class);
            Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(response.getBody());
            IDOL.ErrorResponse errorResponse = new IDOL.ErrorResponse();
            if (m.containsKey("error")) {       // it should contain this
                errorResponse.error = (Integer) m.get("error");
            }
            if (m.containsKey("reason")) {      // it should contain this
                errorResponse.reason = (String) m.get("reason");
            }
            if (errorResponse.error == 503) {   // rate limit, wait 360 sec
                // no good way to sleep or retry in APEX.
                // business logic layer needs to call again with future or scheduler
            }
            persistApiLogCache();
            throw new IDOLException(errorResponse.error+":"+errorResponse.reason);
        }
*/

    }
    
    /**
     * Serialize IDOL.DocumentInput object to JSON string. To prevent null value becoming "null" string in the JSON
     *   string, needs to use custom serialization with JSONGenerator.
     *
     * @param documentInput DocumentInput to be added to the index.
     * @return JSON string of DocumentInput object.
     * @throws IOException 
     */
    private static String serializeDocumentInput(IDOL.DocumentInput documentInput) throws IOException {
        // always there in only one Document

		ObjectMapper mapper = new ObjectMapper();
		mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
		DateFormat df = new SimpleDateFormat("M/d/yyyy");
		mapper.setDateFormat(df);

		String json = mapper.writeValueAsString(documentInput);
        return json;
    }
}
